1608 stories
·
0 followers

C++ futures part 3: The mental model for std::async

2 Shares

We continue our study of C++ futures with std::async.

The more complex case is the std::async. You give std::async a callable and optional parameters to pass to that callable, and you also specify how and when you want the callable to be invoked. You get two axes: Do you want it to be calculated immediately or on demand? Do you want the calculation to happen on the thread that requests the value or on its own thread?

Now, you’d think you could fill out the table like this:

  Immediate On demand (“deferred”)
Same thread Call now and save the value Call when waited
New thread (“async”) Start the thread now Start the thread when waited

However, that’s not what the C++ standard says.

The C++ standard disallows asking for an immediate value on the same thread. There’s really no value to doing it that way, since there is no actual “future”; it all happens in the present! “This future could have been a function call.”

auto f = std::async({}, calculator);

auto result = f.get();

This would, in theory, mean “Immediately call the calculator and wait for it to produce a value. Store it in the source and return a future that fetches the value from that source.” But that’s the same as just

auto result = calculator();

I’m guessing that the C++ standard disallows it because there’s no point.

The C++ standard also says that if you combine both std::launch::deferred and std::launch::async, it doesn’t mean that you get a calculation that lazy-starts on its own thread. Instead, the standard says that the implementation can choose which flag to honor and which to ignore.

So really, the table is

  Immediate On demand (“deferred”)
Same thread Illegal Call when waited
New thread (“async”) Start the thread now Dealer’s choice

In all cases, the result of the callable (either a value or an exception) is stored in the source.

Now, I noted that there’s no point to asking std::async for an immediate on-thread calculation. But maybe you want to do it anyway because you are interfacing with some other component that expects to receive a future. How could you get the same result?

We’ll look at that next time.

The post C++ futures part 3: The mental model for <CODE>std::async</CODE> appeared first on The Old New Thing.

Read the whole story
Share this story
Delete

C++ futures part 2: The mental model for std::promise, std::packaged_task

2 Shares

Last time, we looked at the “state” that powers the std::promise, std::packaged_task, std::async, std::future, and std::sharedfuture classes. Now we’ll see how that “state” (which I call the source) gets used and passed around.

The lifetime of the source is managed by a std::shared_ptr<source<T>>. Everybody who has a reference to the source uses this shared pointer.

The simplest case is std::promise. The operations on std::promise pretty much map directly to the operations on the source we laid out last time.

The next more complicated case is the std::packaged_task. This takes a callable and saves it for later. You can then call get_future to get a future and hand it to somebody as an IOU for “A value will appear here someday.”

When you want to produce a value, you invoke the packaged_task with whatever parameters you want to pass to the original callable. The result of the invocation (either a value or an exception) is then stored in the source object, which can then be retrieved by the future.

You can imagine a packaged task as just wrapping a promise:

template<typename R, typename... Args>
class packaged_task
{
    std::promise<R> p;
    std::function<R(Args...)> func;

public:
    template<typename F>
    packaged_task(F&& f) : func(std::forward<F>(f)) {}

    auto get_future() { return p.get_future(); }

    void operator()(Args... args) {
        try {
            p.set_value(func(args...));
        } catch (...) {
            p.set_exception(std::current_exception());
        }
    }
};

When you invoke it, it calls the stored callable with the arguments and puts the result in the promise.

Okay, those were the easy cases of pushing values into the source. Next time, we’ll look at the messy case: std::async.

The post C++ futures part 2: The mental model for <CODE>std::promise</CODE>, <CODE>std::packaged_task</CODE> appeared first on The Old New Thing.

Read the whole story
Share this story
Delete

C++ futures part 5: std::future and std::shared_future

2 Shares

Wrapping up our quick introduction to the C++ futures library, we come to the last two parts: std::future and std::shared_future.

The std::future is the starting point. It is a move-only class that this takes the source and forwards its wait and wait_for methods to the source, and forwards its get method to consume.

Forwarding the get to consume means that you can call get only once.

The idea is that std::future is for the case where there is exactly one consumer of the value. Since it is move-only, you can’t copy it; you can only give it away and transfer the power to retrieve the value to the recipient.

The std::future has a method called shared(). This transfers the source from the future to a shared_future object. The shared_future also forwards its wait and wait_for methods to the source, but instead of forwarding its get to the source::consume, shared_future::get() just calls source::get to get a reference to the value storage.

This is what makes the “shared future” shared: The shared_future is copyable, so you can have multiple people all with their own shared_future that share the same source, and each one can separately wait for the value and retrieve the value as many times as they want.

The downside of shared_future is that you always get a const reference, so if you want to perform non-const operations on the result, you’ll have to make a copy and operate on the copy. And that’s not possible at all if the result is itself a move-only type, since you can’t move out of a const reference.

You can see that at the end of the day, all of the pieces of the futures library are just different ways of manipulating the invisible source. Some components push values into the source, and others pull values out of the source.

It took me a while to realize that the most important part of the whole futures library is something you can’t even see.

Bonus chatter: All the names in the futures library are horrible. I mean, really, “packaged task”?

Here’s my attempt to give the pieces better names:

Old name New name
promise promise_source
packaged_task function_promise_source
async(deferred) make_lazy_promise
async(async) make_thread_promise
future promise
shared_future shared_promise

I admit, these names aren’t all that good either. But I prefer to use the word “promise” to refer to the thing that “promises” to produce a value. And the pattern promise_source / promise makes the relationship between the two much clearer: A promise source is a thing that makes promises. And it avoids the hilarity of a non-async async().

The post C++ futures part 5: <CODE>std::<WBR>future</CODE> and <CODE>std::<WBR>shared_future</CODE> appeared first on The Old New Thing.

Read the whole story
Share this story
Delete

Bringing Correctly Rounded Math to Production with LLVM-libc

2 Shares

As we mentioned in the last cmath blog post, MSVC is using LLVM-libc for compile-time evaluation and runtime execution of math functions when /Zc:cmath is enabled. I invited LLVM-libc contributors Michael Jones and Tue Ly to write a guest blog post about how they’ve achieved what they’ve achieved. This is that blog post! I hope you enjoy it. I learned a lot from reading it, even after having spent a lot of time in this area.

– Cody, Microsoft C++ Compiler Frontend Engineer

Hi, I’m Michael Jones, the lead maintainer for LLVM-libc. Cody invited me and our math lead, Tue Ly, to write a guest post about LLVM-libc’s math functions. Tue and I work on LLVM-libc at Google, where we’ve been working on the project for about five years now.

I’m going to talk a bit about the history and philosophy of LLVM-libc, then Tue will do a deeper dive into how and why our math functions are so good. I’m really excited for you all to use the code we’ve been working on, and thanks to Cody for putting all of this together!

1. libc as a Library

Generally, implementations of the C standard library (libc) are written for a specific target. They are usually monolithic libraries that are closely tied to the OS interface they’re based on. They also don’t make any effort to keep individual functions separate, which makes them difficult to break into smaller chunks.

In 2019, LLVM-libc was started with a different design philosophy. The focus was on portability, modularity, and writing clean C++ that could be reused. The original design describes this as writing “libc as a library”, meaning that it would be much closer to an idiomatic C/C++ library. From the beginning, there has been a separation between the OS interface layer and the public interface, as well as making each individual function independent. Portability has also involved avoiding assembly except where absolutely necessary, which has the added benefit of enabling the compiler to make deeper optimizations.

Building on the modular design, I started Project Hand-in-Hand which allowed LLVM’s libc++ to use the same float-to-string conversion code as LLVM-libc. Other LLVM projects have also started to integrate LLVM-libc’s code, such as OpenMP using the printf core and compiler-rt using the software floating point support. Soon both clang and MSVC will be using LLVM-libc’s math library for constexpr evaluation, but what makes LLVM-libc’s math suitable for compiler use?

2. libm as an Opportunity

I feel like I know more than the average programmer about floating point numbers, but I would not consider myself an expert. Tue Ly is definitely an expert, with the experience and credentials to prove it. He was the one who designed LLVM-libc’s math library from the ground up, and it was his idea to make it correctly rounded for all rounding modes. This turned out to be hugely important to Google for much the same reason it’s important to Microsoft: different versions of your math library returning different results can have disastrous consequences when working with heterogeneous distributed systems. I’ll let Tue go into the details of exactly how the math functions are still performant while being so accurate, as well as the path to get there.

3. The “Dark Magic” of libm and Why Consistency Demands Correct Rounding

Thanks, Michael! And thanks to Cody for inviting us to contribute to this series.

In the previous blog post, Cody laid out the four dimensions users care about when it comes to math libraries: Stability, Accuracy, Consistency, and Performance (or, in Cody’s words, “Numbers go brrr”).

Floating-point math in software is often treated like dark magic. When developers see different results across platforms or compilers, they often shrug and say, “Well, floating-point is inexact.” But inexact representation does not mean mathematical operations have to be non-deterministic.

Traditional math libraries (libm) frequently produce different results across platforms and versions. In production systems, these differences cause real issues:

  • Instruction-Level Divergence: When hardware designers try to speed up math operations by implementing them directly in silicon (like fast reciprocal square roots or transcendentals), different CPU vendors often make different trade-offs. As a result, the exact same C++ code running on Intel, AMD, ARM, or a GPU can produce slightly different numbers.
  • The Trap of Bug-for-Bug Compatibility: Once an inaccurate math routine ships in a major platform or OS, downstream software inevitably starts relying on those exact bits. A classic example is the legacy x87 hardware transcendental instructions (FSIN, FCOS):
    • Intel’s original hardware implementation used a truncated 66-bit approximation of π for argument reduction. As Bruce Dawson documented in Intel Underestimates Error Bounds by 1.3 quintillion, for certain inputs this causes huge errors, producing inaccuracies of up to 1.37×10 ULPs (Units in the Last Place)!
    • To preserve bug-for-bug compatibility with software expecting Intel x87 outputs, AMD adopted the same 66-bit reduction behavior in their x87 instruction set, locking the x86 ecosystem into the same legacy behavior.
    • On top of that, because these legacy instructions are microcoded in modern processors, they are actually much slower than optimized software implementations using modern range-reduction algorithms like Payne-Hanek.
  • Breaking Golden Tests: Golden tests check that a specific input gives a consistent output, usually by comparing against a fixed “golden” result. These are common for image processing pipelines where stability of results is important. Even in software libraries, if a library only guarantees a loose error bound, internal optimizations or bug fixes can shift output bits. A tiny 1-ULP shift in std::sin or std::pow can invalidate hundreds of golden tests requiring engineering teams to spend time updating test baselines.

The Limits of “Flag-Based” and Environment-Freezing Workarounds

Developers have dealt with floating-point differences across platforms for a long time, as seen across game development, physics simulations, and language standardization:

  • Game Engines & Lockstep Simulations: Analyses by Bruce Dawson (Random ASCII, including articles on intermediate precision), Glenn Fiedler (Gaffer on Games), and Forrest Smith (Planetary Annihilation) show how differences in instructions, compiler optimizations, and math libraries routinely desynchronize multiplayer physics engines.
  • Custom Math Workarounds: When building Factorio, Wube Software found that standard math functions (sin, cos) returned slightly different results between Windows, Linux, and macOS. Without a correctly rounded math library available, they had to write their own custom software trigonometry routines to keep multiplayer games in sync. Similarly, open-source RTS games bundled custom math libraries like STREFLOP (Spring RTS / Beyond All Reason) to get identical calculations across operating systems.
  • C++ Standardization Proposals: The ISO C++ Committee is actively discussing these reproducibility issues across compilers and targets in proposals like P3375 (“Reproducible floating-point results”, Davidson et al.).

However, all of these traditional workarounds share an inherent limitation: while compiler flags and hardware normalization can achieve reproducibility for basic arithmetic operations (+, −, ×, /, √), they fall short for transcendental functions (sin, cos, exp, log, etc.), which form a substantial part of standard math.h.

Basic arithmetic operations are strictly specified and hardware-mandated to be correctly rounded under IEEE-754. But transcendental functions in standard math.h are almost always software approximations where IEEE-754 has historically not mandated bit-level exactness. Consequently, their output bits are simply an artifact of a specific library’s polynomial approximations, range-reduction splits, or lookup table values.

The moment a library maintainer updates a function to make it faster, reduce binary size, or vectorize it, the least significant bits will inevitably shift for many inputs.

Freezing compiler flags or using custom lookup tables cannot protect you from future library updates. You are left choosing between never updating dependencies or frequently updating downstream tests.

Why Correct Rounding Solves This

This is where correct rounding fixes the problem.

Under IEEE-754, if a math function is guaranteed to be correctly rounded across all standard rounding modes (round-to-nearest, round-up, round-down, and round-to-zero), the output bit pattern for any given input is mathematically unique, meaning there is only one correct answer.

By solving for Accuracy through correct rounding, you automatically get Stability and Consistency for free. Because the output is mathematically determined rather than an implementation detail, library authors can completely rewrite, optimize, and vectorize their algorithms across future releases without shifting output bits. Your math functions return the exact same bits on x86-64, ARM64, Windows, Linux, across datacenters, across library updates, and, with C++23 (P0533R9) and C++26 (P1383R2) adding constexpr math, between compile-time evaluation and runtime execution.

4. Standing on the Shoulders of Giants: The Road to Production Correct Rounding

In his blog post, Cody joked that writing a complete, accurate math library seemed to require “years of monk-like study.” Cody wasn’t wrong, except it was decades of research across the computer arithmetic community.

The Table-Maker’s Dilemma

For decades, correct rounding for transcendental functions (eˣ, log x, sin x, etc.) was considered too slow for production systems due to the Table-Maker’s Dilemma.

The dilemma is simple to state but hard to solve: when computing an approximation ŷ of a transcendental function y = f(x), how many extra bits of intermediate precision do you need to guarantee that rounding ŷ produces the exact same floating-point value as rounding the exact mathematical value f(x) ?

Depicts a number line centered on the rounding midpoint between two representable floating-point values. The approximation interval contains the rounding midpoint.

If the true mathematical result falls very close to a rounding midpoint (for round-to-nearest) or a representable floating-point boundary (for directed rounding), an approximation with standard precision cannot tell whether to round up or down.

The first major attempt to bring correctly rounded transcendentals to production was Abraham Ziv’s work on IBM libultim in the 1990s (later used in glibc). Ziv introduced the multi-stage evaluation strategy: compute a fast approximation; if the result is too close to a rounding boundary (Ziv’s rounding test), fall back to a higher-precision path.

However, because the maximum required precision was unknown at the time, libultim‘s fallback path used arbitrary-precision arithmetic scaling up to 768 bits of precision (via internal helpers like slowpow.c and mppow.c). When programs hit one of these hard-to-round inputs, performance dropped sharply: while a normal fast-path pow took ~70 cycles, the 768-bit fallback took up to 440,000 cycles (as documented in glibc Bug 13932 and Bug 16898) which is a ~6,000× slowdown on a single function call!

These unpredictable slowdowns caused performance issues in production workloads, eventually leading glibc maintainers to remove libultim in favor of faster, non-correctly rounded routines with bounded latency. As a result, correct rounding got a reputation for being “too slow for production.”

Hunting the Worst Cases

To avoid these slowdowns, the arithmetic research community needed to answer a fundamental question: What is the worst-case hard-to-round input across the entire floating-point domain? If you know the worst case, you know the maximum precision ever required, and you can build a bounded, non-allocating fallback path.

Finding these worst cases is very difficult. In double precision, searching the 2⁶⁴ input space by brute force was long considered computationally prohibitive.

The breakthrough began with the foundational work of Vincent Lefèvre and Jean-Michel Muller (Arénaire / AriC at ENS Lyon / CNRS / Inria). In pioneering publications like Toward correctly rounded transcendentals (Lefèvre, Muller, & Tisserand, 1998) and Worst Cases for Correct Rounding of the Elementary Functions in Double Precision (Lefèvre & Muller, 2001), they proved that the worst-case precision required for double-precision functions could be systematically determined. Their work culminated in the release of CR-LIBM, demonstrating for the first time that double-precision elementary functions could be correctly rounded with reasonably bounded intermediate precision.

Over the last several years, this search was completed. Through our close collaboration with Paul Zimmermann and Vincent Lefèvre, culminating in our recent paper Computing hard-to-round cases of sin, cos, tan in double precision (ARITH 2026), we have now systematically searched and proven the worst-case bounds for all standard univariate double-precision math functions.

Most importantly, all of these worst-case inputs and mathematical bounds are openly hosted and maintained in the CORE-MATH project repository (and documented at core-math.gitlabpages.inria.fr), serving as an open, shared ground truth for library implementers worldwide.

Bringing Correct Rounding to Production

Around 2020, when we started architecting the math library for LLVM-libc, several independent developments were converging:

  1. Proven Mathematical Bounds: Decades of search algorithms had established the hardest-to-round cases, proving that 128-bit intermediate precision is sufficient for most univariate double-precision functions.
  2. Modern 64-bit Hardware: Modern processors had native hardware Fused Multiply-Add (FMA), wide vector pipelines, and rich register files capable of executing branchless 128-bit integer arithmetic in a handful of cycles.

The realization that the timing was finally right wasn’t unique to LLVM-libc. Around the exact same time, two other major research efforts started with the same objective:

  • The CORE-MATH project led by Paul Zimmermann at Inria, focusing on open-source, correctly rounded reference implementations.
  • The RLIBM project led by Santosh Nagarakatte at Rutgers University, exploring novel polynomial generation techniques for correct rounding.

From the very beginning, the LLVM-libc math team established an active collaboration with CORE-MATH and RLIBM: discussing algorithm approaches, sharing hard-to-round test vectors, developing parallel implementations, and cross-validating each other’s implementations.

This collaboration helped us verify correctness, optimize performance, and prove that correctly rounded math is fast enough for production use.

5. Under the Hood: Making Numbers Go Brrr with Correct Rounding

When developers hear that LLVM-libc guarantees correct rounding, their immediate reaction is often skepticism: “If you’re checking every last bit against the exact mathematical value, how is your library not slow?”

The answer lies in how we structure our execution pipeline around modern hardware strengths:

Depicts the control flow of mathematical functions. All functions execute a fast computation. Greater than 99.99% of inputs pass Ziv's rounding test and return immediately. Those that don't go through a bounded accuracy pass.

When an input comes in, we don’t immediately jump into high-precision math. Instead, our first stage evaluates a fast, carefully tuned Taylor or minimax polynomial approximation using double-double or truncated double-double arithmetic to achieve 70-106 bits of precision, heavily accelerated by native hardware FMA instructions. We break the function’s domain into small intervals such that lookup tables (if needed) comfortably reside in the CPU’s L1 cache.

Once the fast path computes this higher-than-double approximation, we perform Ziv’s rounding test: we check whether the error interval contains a rounding boundary. If the answer is clear (which happens for over 99.99% of all possible inputs), we round to the target precision and return immediately. In terms of latency and throughput, this fast path is very similar to fast, non-correctly rounded math libraries.

What about that rare fraction of a percent (<0.01%) of tricky, hard-to-round inputs where the fast path is inconclusive?

Because the maximum intermediate precision is mathematically proven to never exceed 128 bits, our second stage never needs to invoke an arbitrary-precision library or allocate memory on the heap. Instead, it executes a fixed, branchless 128-bit or 256-bit integer routine with a predictable, bounded cycle count. There are no latency cliffs.

The Freedom to Optimize

Guaranteed correct rounding completely changes the economics of math library development. Historically, maintainers avoided modifying math routines because any tweak, such as changing a polynomial degree or shrinking a lookup table, would shift output bits and break downstream regression tests. With correct rounding, that concern disappears. Because the output is mathematically determined, we are free to explore all kinds of performance improvements: tuning polynomial degrees, optimizing cache footprints, leveraging target-specific hardware instructions (like FMA and SIMD), or rewriting entire fast paths without worrying about breaking downstream software.

Additionally, implementing the entire library in clean, standard C++ gives the compiler optimizers full visibility into the code. Rather than relying on inline assembly or platform-specific constructs that obscure dataflow, modern compilers can freely inline helper functions, schedule instructions to increase instruction-level parallelism, and auto-vectorize loops across different architectures.

Conclusion & Joining the Community

Bringing correctly rounded math into production has been a multi-year effort spanning academic pioneers, open-source maintainers, and industry partners.

Today, software stacks are becoming increasingly complex and distributed. With machine learning models and mixed-precision pipelines already introducing non-determinism, having standard math functions return identical results across platforms and versions helps keep software reliable and predictable.

We are very excited to see MSVC adopting LLVM-libc’s math library for constant evaluation and more in C++23, bringing deterministic floating-point math to all the Windows developers worldwide.

If you’re interested in contributing to LLVM-libc, either for the floating-point math effort or the overall standard library development, we’re always looking for new contributors! You can find more information including how to set up a local build at https://libc.llvm.org/. You can also find the community on the LLVM discord in the #libc channel or join one of our regular public meetings. We hope to see you there!

Michael Jones

Tue Ly

 

The post Bringing Correctly Rounded Math to Production with LLVM-libc appeared first on C++ Team Blog.

Read the whole story
Share this story
Delete

Anthropic Reinvents Source Control As Claude Floods the World With Code

1 Comment
Anthropic says Claude-generated code has scaled so quickly that its existing Git-based infrastructure could not keep up. In response, the company built a custom centralized source-control system called Scion, along with AI-assisted review and search tools. The Register reports: "In May 2026, Anthropic averaged around 2,500 code changes merged per day; as of September, we merged over 30,000 per day," the biz said in a blog post on Tuesday. "We don't expect this rate of growth to slow down anytime soon; the latent demand for merging code is practically infinite, and every bottleneck we remove immediately leads to a further increase in the amount of code we produce." After several months of model prompting and five months of migration, the result is a homemade version control system called Scion, a monorepo scheme that sounds a bit like Subversion, Apache's centralized version control system. [...] Scion has been designed to meet the needs of AI agents, so the client leaves much of the work to the server; the source control queries are exposed via API so internal tools can respond without needing a local repo, and there's less synchronization but more need for server speed. "In Scion, the unit of work is a 'changeset,' an entry in a write-ahead log," Anthropic explains. "A changeset stores most of what you'd expect from a commit in git. But where a git commit stores a pointer to the tree, a changeset stores a list of the files that changed relative to its first parent, along with content-addressed pointers to the compressed file contents in blob storage." [...] Anthropic says its bespoke system isn't right for general use. There will be no open source release. Nonetheless, the company contends that its own code scaling problems will become commonplace in other organizations over the next few months, so it wanted to share its approach -- having ample funding to pay for writing custom infrastructure tools.

Read more of this story at Slashdot.

Read the whole story
Share this story
Delete
1 public comment
sean_e
2 days ago
reply
Sounds like perforce...
Arlington, VA

How to properly clean your solar panels (and why it's important)

1 Share
Dirt can cut solar output, but cleaning only pays off when the numbers make sense.

Read the whole story
Share this story
Delete
Next Page of Stories